iT邦幫忙

2023 iThome 鐵人賽

DAY 22
0
Modern Web

網頁的另一個大腦:從基礎到進階掌握 Web Worker 技術系列 第 22

在 Web worker 中處理音效 - AudioWorklet

  • 分享至 

  • xImage
  •  

什麼是 Worklet?

官方文件提到 Worklet 是輕量級的 web worker,使網路開發人員可以存取低階的渲染管道

Worklet 種類

MDN 上列出來的有三種,但其中的 AnimationWorkletLayoutWorklet 似乎都還在實驗階段,有興趣瞭解的人可以參考連結所附的介紹文章

而今天打算只介紹控制音訊的 AudioWorklet
https://ithelp.ithome.com.tw/upload/images/20231006/20162687iLnueUs46g.png

AudioWorklet

AudioWorklet 出現之前,瀏覽器提供了 ScriptProcessorNode 供開發者在網頁上處理音訊,但使用 ScriptProcessorNode 操作音訊是執行在主線程上面的,因此有可能導致 UI 畫面的阻塞,所以在 Chrome 64 後才推出了 AudioWorkletAudioWorklet 運行在額外的音訊線程(AudioWorkletGlobalScope),避免了主線程資源的佔用

範例

以下我們直接透過 Google AudioWorklet example,實際看看怎麼在網頁上操作音訊
範例中在按下按鈕後網頁會播放一聲 440Hz 的聲音,請大家在操作前先把聲音調小,不然可能會突然嚇到

https://ithelp.ithome.com.tw/upload/images/20231006/20162687ySBBx0eKGd.png

建立 AudioContext
首先所有音訊操作的都需要在一個 音訊上下文中(AudioContext),所以一開始會先建立 AudioContext,接著再按下 START 按鈕後執行 startAudio

const audioContext = new AudioContext();

window.addEventListener('load', async () => {
  const buttonEl = document.getElementById('button-start');
  buttonEl.disabled = false;
  buttonEl.addEventListener('click', async () => {
    await startAudio(audioContext);
    audioContext.resume();
    buttonEl.disabled = true;
    buttonEl.textContent = 'Playing...';
  }, false);
});

開始播放音訊

const startAudio = async (context) => {
  await context.audioWorklet.addModule('bypass-processor.js');
  const oscillator = new OscillatorNode(context);
  const bypasser = new AudioWorkletNode(context, 'bypass-processor');
  oscillator.connect(bypasser).connect(context.destination);
  oscillator.start();
};

首先會呼叫 Worklet.addModule 將自定義的 bypass-processor.js (後面會再介紹到這個檔案) 以模組方式加載到 AudioContext

await context.audioWorklet.addModule('bypass-processor.js');

接著呼叫 OscillatorNodeOscillatorNode 可以建構新的音訊波形,預設波形是正弦波(sine)、頻率是 440 Hz

// context - 使用的 AudioContext 
// options - 可以設定波形、頻率等參數
const oscillator = new OscillatorNode(context, options);

例如:以下建立 660 Hz 的正弦波

const options = {
  type: 'sine',
  frequency: 660
}
const oscillator = new OscillatorNode(context, options);

下一步則是建立 AudioWorkletNode 實例,AudioWorkletNode 代表一個自定義的音訊節點,第二個參數就是我們之前加載進來的檔案名稱

const bypasser = new AudioWorkletNode(context, 'bypass-processor');

接著會將建立好的 OscillatorNode 連接到自定義的 bypasser(AudioWorkletNode),再連接到整個 AudioContext 的終點 (destination),這個終點可以視為播放聲音出來的設備,例如喇叭

oscillator.connect(bypasser).connect(context.destination);

最後呼叫 start 方法,開始播放音訊

oscillator.start();

音訊路由圖

上面這一連串使用到的方法,乍看之下似乎難以理解,但搭配以下這張 音訊路由圖 大概會有點感覺,整個音訊的操作都需要在黃色範圍內的 AudioContext 裡處理,而以上程式碼所做的事情大部分就是創立 音訊節點(AudioNode),並把各節點以 connect 的方式連接起來,當所有音訊節點的關聯性都連結好後,最後一步是呼叫 connect(context.destination),將所有節點連接到右下角的擴音設備,準備進行播放
https://ithelp.ithome.com.tw/upload/images/20231006/20162687xn7nuOtt6h.png

創建自訂音訊

在創建自訂音訊的時候有以下幾個步驟:

  1. 建立一個單獨的檔案,即 bypass-processor.js
  2. 在檔案中需要擴展 AudioWorkletProcessor 類別,並且其中需要提供 process() 方法
  3. 需要呼叫 registerProcessor 方法,註冊第二步中建立的類別
  4. 使用 Worklet.addModule 將檔案載入到 AudioContext
  5. 藉由 AudioWorkletNode 實例將第二步自定義的類別在其中實例化
  6. 將音訊節點連接到其他節點 (就是以上所說的 connect)

其中 1~3 步驟如以下所示,而 4~6 步驟是上面的 startAudio 函式中提過的

自訂音訊檔案

以下 bypass-processor.js 檔案負責處理自訂音訊,inputoutput 分別對應輸入跟輸出的音源,其中的 BypassProcessor 單純複製輸入的音源到輸出,沒有做任何額外處理,process 方法的 回傳值 為 true 強制使 AudioWorkletNode 的狀態是 active

最後一行呼叫 registerProcessor 註冊 BypassProcessor 並給他一個名稱 'bypass-processor'

// bypass-processor.js
class BypassProcessor extends AudioWorkletProcessor {
  process(inputs, outputs) {
    // 預設只有單一個 input 跟 output.
    const input = inputs[0];
    const output = outputs[0];

    // 單純複製輸入的音源到輸出,沒有做任何額外處理
    for (let channel = 0; channel < output.length; ++channel) {
      output[channel].set(input[channel]);
    }

    return true;
  }
}

registerProcessor('bypass-processor', BypassProcessor);

小結

結合以上程式碼,範例最終會輸出一個 440 Hz 的正弦波。

以上範例沒有額外處理音訊的輸入、輸出,但要寫出程式碼還是蠻複雜的,實在沒想到在網頁中處理音訊這麼困難

總之最後藉由這個範例大致上瞭解了 AudioWorklet 的用法,以及知道它可以使用額外獨立的線程處理音訊,避免影響到主線程 UI 的渲染

Reference

Enter Audio Worklet


上一篇
Javascript 中的原子操作 - Atomic
下一篇
瀏覽器中的消息傳遞 - postMessage
系列文
網頁的另一個大腦:從基礎到進階掌握 Web Worker 技術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言